/**
 * A control that allows selection of multiple items in a list.
 */
Ext.define('Ext.ux.form.MultiSelect', {

	extend : 'Ext.form.FieldContainer',

	mixins : {
		bindable : 'Ext.util.Bindable',
		field : 'Ext.form.field.Field'
	},

	alternateClassName : 'Ext.ux.Multiselect',
	alias : ['widget.multiselectfield', 'widget.multiselect'],

	requires : ['Ext.panel.Panel', 'Ext.view.BoundList',
			'Ext.layout.container.Fit'],

	uses : ['Ext.view.DragZone', 'Ext.view.DropZone'],

	layout : 'anchor',

	/**
	 * @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect
	 *      DragZone.
	 */

	/**
	 * @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect
	 *      DropZone.
	 */

	/**
	 * @cfg {String} [title=""] A title for the underlying panel.
	 */

	/**
	 * @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect
	 *      list are drag/drop reorderable.
	 */
	ddReorder : false,

	/**
	 * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of
	 *      the control's selection list. This can be a
	 *      {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of
	 *      buttons/button configs to be added to the toolbar. See
	 *      {@link Ext.panel.Panel#tbar}.
	 */

	/**
	 * @cfg {String} [appendOnly=false] `true` if the list should only allow
	 *      append drops when drag/drop is enabled. This is useful for lists
	 *      which are sorted.
	 */
	appendOnly : false,

	/**
	 * @cfg {String} [displayField="text"] Name of the desired display field in
	 *      the dataset.
	 */
	displayField : 'text',

	/**
	 * @cfg {String} [valueField="text"] Name of the desired value field in the
	 *      dataset.
	 */

	/**
	 * @cfg {Boolean} [allowBlank=true] `false` to require at least one item in
	 *      the list to be selected, `true` to allow no selection.
	 */
	allowBlank : true,

	/**
	 * @cfg {Number} [minSelections=0] Minimum number of selections allowed.
	 */
	minSelections : 0,

	/**
	 * @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of
	 *      selections allowed.
	 */
	maxSelections : Number.MAX_VALUE,

	/**
	 * @cfg {String} [blankText="This field is required"] Default text displayed
	 *      when the control contains no items.
	 */
	blankText : 'This field is required',

	/**
	 * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"]
	 *      Validation message displayed when {@link #minSelections} is not met.
	 *      The {0} token will be replaced by the value of
	 *      {@link #minSelections}.
	 */
	minSelectionsText : 'Minimum {0} item(s) required',

	/**
	 * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"] Validation
	 *      message displayed when {@link #maxSelections} is not met The {0}
	 *      token will be replaced by the value of {@link #maxSelections}.
	 */
	maxSelectionsText : 'Maximum {0} item(s) required',

	/**
	 * @cfg {String} [delimiter=","] The string used to delimit the selected
	 *      values when {@link #getSubmitValue submitting} the field as part of
	 *      a form. If you wish to have the selected values submitted as
	 *      separate parameters rather than a single delimited parameter, set
	 *      this to `null`.
	 */
	delimiter : ',',

	/**
	 * @cfg String [dragText="{0} Item{1}"] The text to show while dragging
	 *      items. {0} will be replaced by the number of items. {1} will be
	 *      replaced by the plural form if there is more than 1 item.
	 */
	dragText : '{0} Item{1}',

	/**
	 * @cfg {Ext.data.Store/Array} store The data source to which this
	 *      MultiSelect is bound (defaults to `undefined`). Acceptable values
	 *      for this property are: <div class="mdetail-params">
	 *      <ul>
	 *      <li><b>any {@link Ext.data.Store Store} subclass</b></li>
	 *      <li><b>an Array</b> : Arrays will be converted to a
	 *      {@link Ext.data.ArrayStore} internally. <div class="mdetail-params">
	 *      <ul>
	 *      <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div
	 *      class="sub-desc"> A 1-dimensional array will automatically be
	 *      expanded (each array item will be the combo
	 *      {@link #valueField value} and {@link #displayField text})</div></li>
	 *      <li><b>2-dimensional array</b> : (e.g.,
	 *      <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc"> For a
	 *      multi-dimensional array, the value in index 0 of each item will be
	 *      assumed to be the combo {@link #valueField value}, while the value
	 *      at index 1 is assumed to be the combo {@link #displayField text}.
	 *      </div></li>
	 *      </ul>
	 *      </div></li>
	 *      </ul>
	 *      </div>
	 */

	ignoreSelectChange : 0,

	/**
	 * @cfg {Object} listConfig An optional set of configuration properties that
	 *      will be passed to the {@link Ext.view.BoundList}'s constructor. Any
	 *      configuration that is valid for BoundList can be included.
	 */

	initComponent : function() {
		var me = this;

		me.bindStore(me.store, true);
		if (me.store.autoCreated) {
			me.valueField = me.displayField = 'field1';
			if (!me.store.expanded) {
				me.displayField = 'field2';
			}
		}

		if (!Ext.isDefined(me.valueField)) {
			me.valueField = me.displayField;
		}
		me.items = me.setupItems();

		me.callParent();
		me.initField();
		me.addEvents('drop');
	},

	setupItems : function() {
		var me = this;

		me.boundList = Ext.create('Ext.view.BoundList', Ext.apply({
							anchor : 'none 100%',
							deferInitialRefresh : false,
							border : 1,
							multiSelect : true,
							store : me.store,
							displayField : me.displayField,
							disabled : me.disabled
						}, me.listConfig));
		me.boundList.getSelectionModel().on('selectionchange',
				me.onSelectChange, me);

		// Only need to wrap the BoundList in a Panel if we have a title.
		if (!me.title) {
			return me.boundList;
		}

		// Wrap to add a title
		me.boundList.border = false;
		return {
			border : true,
			anchor : 'none 100%',
			layout : 'anchor',
			title : me.title,
			tbar : me.tbar,
			items : me.boundList
		};
	},

	onSelectChange : function(selModel, selections) {
		if (!this.ignoreSelectChange) {
			this.setValue(selections);
		}
	},

	getSelected : function() {
		return this.boundList.getSelectionModel().getSelection();
	},

	// compare array values
	isEqual : function(v1, v2) {
		var fromArray = Ext.Array.from, i = 0, len;

		v1 = fromArray(v1);
		v2 = fromArray(v2);
		len = v1.length;

		if (len !== v2.length) {
			return false;
		}

		for (; i < len; i++) {
			if (v2[i] !== v1[i]) {
				return false;
			}
		}

		return true;
	},

	afterRender : function() {
		var me = this, records;

		me.callParent();
		if (me.selectOnRender) {
			records = me.getRecordsForValue(me.value);
			if (records.length) {
				++me.ignoreSelectChange;
				me.boundList.getSelectionModel().select(records);
				--me.ignoreSelectChange;
			}
			delete me.toSelect;
		}

		if (me.ddReorder && !me.dragGroup && !me.dropGroup) {
			me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
		}

		if (me.draggable || me.dragGroup) {
			me.dragZone = Ext.create('Ext.view.DragZone', {
						view : me.boundList,
						ddGroup : me.dragGroup,
						dragText : me.dragText
					});
		}
		if (me.droppable || me.dropGroup) {
			me.dropZone = Ext.create('Ext.view.DropZone', {
				view : me.boundList,
				ddGroup : me.dropGroup,
				handleNodeDrop : function(data, dropRecord, position) {
					var view = this.view, store = view.getStore(), records = data.records, index;

					// remove the Models from the source Store
					data.view.store.remove(records);

					index = store.indexOf(dropRecord);
					if (position === 'after') {
						index++;
					}
					store.insert(index, records);
					view.getSelectionModel().select(records);
					me.fireEvent('drop', me, records);
				}
			});
		}
	},

	isValid : function() {
		var me = this, disabled = me.disabled, validate = me.forceValidation
				|| !disabled;

		return validate ? me.validateValue(me.value) : disabled;
	},

	validateValue : function(value) {
		var me = this, errors = me.getErrors(value), isValid = Ext
				.isEmpty(errors);

		if (!me.preventMark) {
			if (isValid) {
				me.clearInvalid();
			} else {
				me.markInvalid(errors);
			}
		}

		return isValid;
	},

	markInvalid : function(errors) {
		// Save the message and fire the 'invalid' event
		var me = this, oldMsg = me.getActiveError();
		me.setActiveErrors(Ext.Array.from(errors));
		if (oldMsg !== me.getActiveError()) {
			me.updateLayout();
		}
	},

	/**
	 * Clear any invalid styles/messages for this field.
	 * 
	 * __Note:__ this method does not cause the Field's {@link #validate} or
	 * {@link #isValid} methods to return `true` if the value does not _pass_
	 * validation. So simply clearing a field's errors will not necessarily
	 * allow submission of forms submitted with the
	 * {@link Ext.form.action.Submit#clientValidation} option set.
	 */
	clearInvalid : function() {
		// Clear the message and fire the 'valid' event
		var me = this, hadError = me.hasActiveError();
		me.unsetActiveError();
		if (hadError) {
			me.updateLayout();
		}
	},

	getSubmitData : function() {
		var me = this, data = null, val;
		if (!me.disabled && me.submitValue && !me.isFileUpload()) {
			val = me.getSubmitValue();
			if (val !== null) {
				data = {};
				data[me.getName()] = val;
			}
		}
		return data;
	},

	/**
	 * Returns the value that would be included in a standard form submit for
	 * this field.
	 * 
	 * @return {String} The value to be submitted, or `null`.
	 */
	getSubmitValue : function() {
		var me = this, delimiter = me.delimiter, val = me.getValue();

		return Ext.isString(delimiter) ? val.join(delimiter) : val;
	},

	getValue : function() {
		return this.value || [];
	},

	getRecordsForValue : function(value) {
		var me = this, records = [], all = me.store.getRange(), valueField = me.valueField, i = 0, allLen = all.length, rec, j, valueLen;

		for (valueLen = value.length; i < valueLen; ++i) {
			for (j = 0; j < allLen; ++j) {
				rec = all[j];
				if (rec.get(valueField) == value[i]) {
					records.push(rec);
				}
			}
		}

		return records;
	},

	setupValue : function(value) {
		var delimiter = this.delimiter, valueField = this.valueField, i = 0, out, len, item;

		if (Ext.isDefined(value)) {
			if (delimiter && Ext.isString(value)) {
				value = value.split(delimiter);
			} else if (!Ext.isArray(value)) {
				value = [value];
			}

			for (len = value.length; i < len; ++i) {
				item = value[i];
				if (item && item.isModel) {
					value[i] = item.get(valueField);
				}
			}
			out = Ext.Array.unique(value);
		} else {
			out = [];
		}
		return out;
	},

	setValue : function(value) {
		var me = this, selModel = me.boundList.getSelectionModel(), store = me.store;

		// Store not loaded yet - we cannot set the value
		if (!store.getCount()) {
			store.on({
						load : Ext.Function.bind(me.setValue, me, [value]),
						single : true
					});
			return;
		}

		value = me.setupValue(value);
		me.mixins.field.setValue.call(me, value);

		if (me.rendered) {
			++me.ignoreSelectChange;
			selModel.deselectAll();
			selModel.select(me.getRecordsForValue(value));
			--me.ignoreSelectChange;
		} else {
			me.selectOnRender = true;
		}
	},

	clearValue : function() {
		this.setValue([]);
	},

	onEnable : function() {
		var list = this.boundList;
		this.callParent();
		if (list) {
			list.enable();
		}
	},

	onDisable : function() {
		var list = this.boundList;
		this.callParent();
		if (list) {
			list.disable();
		}
	},

	getErrors : function(value) {
		var me = this, format = Ext.String.format, errors = [], numSelected;

		value = Ext.Array.from(value || me.getValue());
		numSelected = value.length;

		if (!me.allowBlank && numSelected < 1) {
			errors.push(me.blankText);
		}
		if (numSelected < me.minSelections) {
			errors.push(format(me.minSelectionsText, me.minSelections));
		}
		if (numSelected > me.maxSelections) {
			errors.push(format(me.maxSelectionsText, me.maxSelections));
		}
		return errors;
	},

	onDestroy : function() {
		var me = this;

		me.bindStore(null);
		Ext.destroy(me.dragZone, me.dropZone);
		me.callParent();
	},

	onBindStore : function(store) {
		var boundList = this.boundList;

		if (boundList) {
			boundList.bindStore(store);
		}
	}

});
